package pl.edu.agh.gt.ga;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.log4j.Logger;

import pl.edu.agh.gt.gui.ChartDisplay;
import pl.edu.agh.gt.util.BetterRandom;

public class Population {

	private final static Logger log = Logger.getLogger(Population.class);

	private List<Individual> individuals; // the order is constantly shuffled !

	private final ExecutorService threadPool = Executors.newFixedThreadPool(4);
	private List<Callable<Float>> tasks;

	private final FitnessFunction fitnessFun;
	private final MutationOperator mutationOp;
	private final CrossbreedOperator crossbreadOp;
	private final Initializer initializer;

	Parameters params;

	// runtime variables
	int iteration;
	Individual bestIndividual;
	double meanFitness;
	double sigma;
	double sigmaPlus;
	double sigmaMinus;

	public Population(Parameters params, Initializer initializer, FitnessFunction fitness, MutationOperator mutation,
			CrossbreedOperator xbreed) {
		this.params = params;
		fitnessFun = fitness;
		this.initializer = initializer;
		crossbreadOp = xbreed;
		this.mutationOp = mutation;
		Individual.deathCounterInit = params.deathCounterInit;
		tasks = new ArrayList<Callable<Float>>(params.initialPopulationSize * 2);
	}

	private class IndividualEvaluationTask implements Callable<Float> {

		private final Individual ind;
		private final Parameters params;

		public IndividualEvaluationTask(Individual i, Parameters p) {
			ind = i;
			params = p;
		}

		@Override
		public Float call() throws Exception {
			ind.fitness = fitnessFun.estimate(ind.genotype, params);
			return ind.fitness;
		}
	}

	private void mutation() throws InterruptedException {

		tasks.clear();

		for (Individual ind : individuals) {

			float mutationProbability = params.mutationFactor;
			// reduce probability of mutation for well fitted individuals
			mutationProbability *= Math.sqrt(1.0 - (ind.fitness / bestIndividual.fitness));

			if (BetterRandom.nextFloat("mp") < mutationProbability) {
				ind.genotype = mutationOp.mutate(ind.genotype, params);
				addEvalTask(ind);
			}
		}

		threadPool.invokeAll(tasks);
	}

	private void crossbreed() throws InterruptedException {

		tasks.clear();

		// XXX moznaby wielokrotnie jednego osobnika
		List<Individual> parents = tournamentSelection(
				(int) 2.0 * Math.round(individuals.size() * params.crossbreedFactor), false);

		Iterator<Individual> it = parents.iterator();
		while (it.hasNext()) {

			Individual parent1 = it.next();
			if (!it.hasNext())
				break;
			Individual parent2 = it.next();

			Genotype newGens = crossbreadOp.crossbreed(parent1.genotype, parent2.genotype, params);

			Individual newInd = new Individual(newGens);
			addEvalTask(newInd);
			individuals.add(newInd);
		}

		threadPool.invokeAll(tasks);
	}

	private void reinsertion() {

		// decrease life
		// remove dead individuals
		int oversize = individuals.size() - params.initialPopulationSize;
		Collections.sort(individuals);

		Iterator<Individual> iter = individuals.iterator();
		for (int i = 0; i < oversize; i++) {
			Individual l = iter.next();
			l.deathCounter--;
			if (l.deathCounter <= 0)
				iter.remove();
		}

	}

	/**
	 * Tournament selection of N individuals (best or worse)
	 */
	private List<Individual> tournamentSelection(int winnersSetSize, boolean inverse) {

		List<Individual> winners = new ArrayList<Individual>();

		Collections.shuffle(individuals, BetterRandom.getInstance("ts"));

		float tournamentSize = ((float) individuals.size() / winnersSetSize);
		float endIdx = tournamentSize;

		ListIterator<Individual> iter = individuals.listIterator();
		Individual winner = iter.next();

		while (iter.hasNext()) {

			if (iter.nextIndex() == (int) Math.round(endIdx)) {
				winners.add(winner);
				winner = iter.next();
				endIdx += tournamentSize;
			}

			Individual curr = iter.next();

			if (inverse) {
				if (curr.fitness < winner.fitness)
					winner = curr;
			} else {
				if (curr.fitness > winner.fitness)
					winner = curr;
			}

		}

		return winners;
	}

	private void addEvalTask(final Individual ind) {

		tasks.add(new IndividualEvaluationTask(ind, params));
	}

	public void initializePopulation() {

		individuals = initializer.initialize(params);

		evaluatePopulation();
	}

	public void evolvePopulation() throws InterruptedException {

		iteration = 0;

		// XXX warunek stopu gdy fitness sie stabilizuje ?
		// moze gdy histogram sie zmienia na 'nienormalny' - semiwariancja
		// dodatnia ~ 0
		while (iteration < params.maxIterations) {

			// diagnostics ...
			diag();

			// mutate
			mutation();

			// X breed
			crossbreed();

			// selection & reinsertion
			reinsertion();

			// diagnostics ...
			calculateStats();

			iteration++;
		}

	}

	private void diag() {

		log.info("iter: " + iteration + ", population: " + individuals.size() + ", mean fitness: "
				+ (float) meanFitness + ", sigma: " + (float) sigma + ", sigma+ : " + (float) sigmaPlus + " sigma- : "
				+ (float) sigmaMinus);

		log.info("Current best: " + bestIndividual);

		if (iteration % 10 == 0) {
			ChartDisplay.fitnessHistogram("Fitness for iteration " + iteration, individuals);
			ChartDisplay.genotypeSpaceCoverage("Space coverage for iter " + iteration, individuals, "desiredEarn",
					"enteringValue");
			// ChartDisplay.genToFitnessRelation("Gen to fitness relation "+
			// iteration, individuals, "enteringValue");
		}

	}

	private void calculateStats() {
		// calculate best, mean fitness, variance etc
		double fitnessSum = 0;
		for (Individual ind : individuals) {
			fitnessSum += ind.fitness;
			if (ind.fitness > bestIndividual.fitness) {
				bestIndividual = ind;
			}
		}
		meanFitness = fitnessSum / individuals.size();

		double semiVariancePos = 0;
		double semiVarianceNeg = 0;
		for (Individual ind : individuals) {
			double diff = ind.fitness - meanFitness;
			if (diff < 0)
				semiVarianceNeg += diff * diff;
			else
				semiVariancePos += diff * diff;
		}
		semiVariancePos /= individuals.size();
		semiVarianceNeg /= individuals.size();
		sigmaPlus = Math.sqrt(semiVariancePos);
		sigmaMinus = Math.sqrt(semiVarianceNeg);
		sigma = Math.sqrt(semiVarianceNeg + semiVariancePos);
	}

	public void evaluatePopulation() {
		bestIndividual = individuals.get(0);
		for (Individual ind : individuals) {
			ind.fitness = fitnessFun.estimate(ind.genotype, params);
		}
		calculateStats();
	}

	public Individual getBestIndividual() {
		return bestIndividual;
	}

	public List<Individual> getNBestIndividuals(int n) {
		ArrayList<Individual> list = new ArrayList<Individual>(individuals);
		Collections.sort(list);
		return list.subList(list.size() - n, list.size());
	}

}
